# JS 基础知识点

# 1、js 中存在 7 种原始类型

boolean,null,undefined,number,string,symbol,bigint;typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug

# 2、判断类型

JS 检测数据类型的 4 种方式:

  • typeof
  • instanceof
  • constructor
  • Object.prototype.toString.call()

这里是对这四种方法的简要介绍以及它们在不同场景下的用途、优点和缺点:

typeof:

  • 用途:typeof是一元操作符,用于检测给定值的数据类型。
  • 优点:简单、快速,并且对于大多数基本数据类型(如字符串、数字、布尔值、函数)有效。
  • 缺点:对于复杂数据类型(如数组、对象、null),返回的结果并不具体,都会返回"object"。此外,对于函数来说,返回的是"function",而不是"object"。

instanceof:

  • 用途:instanceof用于检测对象是否属于某个特定的构造函数。
  • 优点:可以判断对象是否是特定构造函数的实例,适用于自定义类型的检测。
  • 缺点:不能准确判断基本数据类型,也不能判断两个不同的全局执行环境中创建的对象。

constructor:

  • 用途:constructor是对象的属性,指向创建该对象的构造函数。
  • 优点:可以判断对象的构造函数,适用于自定义类型的检测。
  • 缺点:当对象的原型被修改时,constructor属性可能会失效。此外,对于基本数据类型和null,无法使用该方法进行检测。

Object.prototype.toString.call():

  • 用途:通过调用 Object.prototype.toString 方法,可以返回一个表示对象类型的字符串。
  • 优点:可以准确判断各种数据类型,包括基本数据类型、内置对象、自定义对象以及特殊的对象(如数组、日期等)。
  • 缺点:使用起来相对繁琐,需要调用特定的方法。

其中最通用的办法是运用Object.prototype.toString.call()

# 基本数据类型和引用数据类型的区别?

比较 基本数据类型 引用数据类型
数据存放位置 基本数据类型存放在栈中,数据大小确定,内存空间大小可以分配 引用数据类型存放在堆中,每个空间大小不一样,要根据情况进行特定的配置
变量存储内容 变量中存储的是值本身 变量存储的是地址
变量用来赋值时 把变量的值复制一份去赋值 把变量的内存地址复制一份去赋值
存储内容大小 存储值较小 存储值较大

栈和堆的的介绍

比较 栈(线程) 堆(进程,线程共享)
大小固定 创建时,确定大小(值大小固定),故可能会溢出 大小不固定,可随时增加
存储类型 存储基本数据类型及引用类型数据的堆地址 存储引用类型数据,当一个引用类型的数据不再被引用时,垃圾回收机制会自动回收这部分内存
如何访问 按值访问 按引用(堆内存地址)访问
特点 空间小,运行效率高 空间大,运行效率相对较低
存放规则 按顺序存放,先进后出 无序存储,可根据引用(地址)直接获取

为什么要这样设计?

这种设计的主要考虑是出于性能和内存管理的角度:

  1. 堆的动态分配和释放:引用类型的数据大小不确定,需要在运行时动态分配内存空间。堆的设计可以灵活地处理对象的创建和销毁,避免了静态分配内存带来的空间浪费。

  2. 栈的高效操作:基本类型和引用类型的变量引用存储在栈中,栈的操作速度较快,因为它使用了简单的指针操作。这种设计可以提高变量的访问速度和内存的利用效率。

  3. 垃圾回收:JavaScript 中使用垃圾回收机制来自动回收不再使用的内存。堆的设计允许动态分配和释放内存,垃圾回收机制可以根据引用的情况自动回收不再使用的堆内存,避免了内存泄漏和资源浪费。

总的来说,堆和栈的设计在 JavaScript 中旨在提供灵活的内存管理和高效的操作,使得开发者可以更方便地处理不同类型的数据,并且在运行时能够自动管理内存的分配和释放。

实例:

基本类型和引用类型赋值的区别

<script>
  var a = 10;
  var b = a; //10
  a = 20;
  console.log(a, b); //20 10
</script>
1
2
3
4
5
6
  • a 变量是基本数据类型,他在赋值是把 a 的值复制一份给到 b
  • 所以 b = 10 ,最后我们修改 a 的值,并不会响影到 b

基本类型和引用类型赋值的区别

<script>
  var obj = {
    name: "清心",
    age: 34,
  };
  var obj2 = obj; //是把obj中地址赋值给obj2,两者指向的是同一个堆内存中地址
  obj.name = "小丽"; //操作是堆内存中的数据
  console.log(obj === obj2); //true
  console.log(obj.name, obj2.name); //小丽 小丽
</script>
1
2
3
4
5
6
7
8
9
10

栈内存中存着变量 obj 对堆内存中的地址,而堆内存中存着对应地址的数据内容

基本类型和引用类型赋值的区别

当 obj2=obj 时,其实是把 obj 中保存的地址赋值给了 obj2,所以本质上 obj===obj2 比较时,比较的是地址,地址始终没有变,所以两者是全等的。

我们在操 obj.name = '小丽' 时,操作的是堆内存中的数据,所以 obj.name 和 obj2.name 的值是一样的。

# 3、关于条件判断

在条件判断时,除了 undefined, null, false, NaN, '', 0, -0,其他所有值都转为 true,包括所有对象

# 4、关于this指向

如果用一句话说明 this 的指向,那么即是: 谁调用它,this 就指向谁。

但是仅通过这句话,我们很多时候并不能准确判断 this 的指向。因此我们需要借助一些规则去帮助自己:

this 的指向可以按照以下顺序判断:

全局环境中的 this

浏览器环境:无论是否在严格模式下,在全局执行环境中(在任何函数体外部)this 都指向全局对象 window;

node 环境:无论是否在严格模式下,在全局执行环境中(在任何函数体外部),this 都是空对象 {};

是否是 new 绑定

如果是 new 绑定,并且构造函数中没有返回 function 或者是 object,那么 this 指向这个新对象。如下:

  • 构造函数返回值不是 function 或 object。new Super() 返回的是 this 对象。
  • 构造函数返回值是 function 或 object,new Super()是返回的是Super种返回的对象。
function Super(age){
  this.age = age
}

let instance = new Super('26')
console.log(instance.age) // 26
1
2
3
4
5
6
function Super(age){
  this.age = age
  let obj = {a:'2'}
  return obj
}

let instance = new Super('26')
console.log(instance) // {a:'2'}
console.log(instance.age) // undefined
1
2
3
4
5
6
7
8
9

函数是否通过 call,apply 调用,或者使用了 bind 绑定,如果是,那么this绑定的就是指定的对象【归结为显式绑定】。

function info(){
  console.log(this.age)
}
var person= {
  age: 20,
  info
}
var age = 28
var info = person.info
info.call(person) // 20
info.apply(person) // 20
info.bind(person)() // 20
1
2
3
4
5
6
7
8
9
10
11
12

这里同样需要注意一种特殊情况,如果 call,apply 或者 bind 传入的第一个参数值是 undefined 或者 null,严格模式下 this 的值为传入的值 null /undefined。非严格模式下,实际应用的默认绑定规则,this 指向全局对象(node环境为global,浏览器环境为window)

function info () {
  // node 环境中:非严格模式 globao,严格模式 null
  // 浏览器环境中:非严格模式 window, 严格模式为 null
  console.log(this)
  console.log(this.age)
}
var person = {
  age: 20,
  info
}
var age = 28
var info = person.info
// 严格模式抛出错误
// 非严格模式,node 下输出 undefined(因为全局的 age 不会挂在 global 上)
// 非严格模式,浏览器环境下输出28(因为全局的 age 回挂在 window 上)
person.info(); // 2O
info(); // 28 注意这里直接调用和赋值引用调用结果不同
info.call(null) // 28
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

注意:不管我们连续给函数 bind 几次,fn 中的 this 永远由第一次 bind 决定:fn.bind().bind(a)() // this 指向fn

隐式绑定,函数的调用是在某个对象上触发的,即调用位置上存在上下文对象。典型的隐式调用为: xxx.fn()

function info(){
  console.log(this.age)
}
var person= {
  age: 20,
  info
}
var age = 28
person.info() // 20,执行的是隐式绑定
1
2
3
4
5
6
7
8
9

默认绑定,在不能应用其它绑定规则时使用的默认规则,通常是独立函数调用。

非严格模式: node环境,指向全局对象 global,浏览器环境,指向全局对象 window。

严格模式:执行 undefined

function info(){
  console.log(this.age)
}

var age = 28
// 严格模式浏览器环境和node环境都抛错
// 非严格模式:node下输出 undefined(因为全局的 age 不会挂在 global 上)
// 非严格模式:浏览器环境下输出 28(因为全局的 age 回挂在 window 上)
info()
1
2
3
4
5
6
7
8
9

箭头函数的情况

箭头函数没有自己的this,继承外层上下文绑定的this。

let obj = {
  age: 20,
  info: function() {
    return () => {
	  console.log(this.age)
	}
  }
}
let person = {age: 28}
let info = obj.info()
info() // 20

let info2 = obj.info.call(person)
info2() // 28
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 5、== 和 ===

== 和 === 的区别是:对于 == 来说,如果对比双方的类型不一样的话,就会进行类型转换

# 6、闭包

闭包的定义:函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包

闭包的应用场景 (opens new window)

  • 函数防抖
  • 使用闭包设计单例模式
  • 为多个组件独立属性
  • 设置私有变量
  • 拿到正确的值
for(var i=0;i<10;i++){
  setTimeout(function(){
    console.log(i)//10个10
  },1000)
}
1
2
3
4
5

# 作用域

# 什么是作用域 ?

  • 作用域就是代码的执行环境。执行环境定义了变量或函数有没有权访问其他数据。
  • 每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。
  • 虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它。

比如特殊的全局执行环境中的变量对象 window 对象,因此所有全局变量和函数都作为 window 对象的属性和方法创建的。在 Node 环境中,全局执行环境是 global 对象

<script>
  var a = 1;
  function sum(a, b) {
    return a + b;
  }
  var n = window.sum(2, 3); // sum 相当于window对象上的方法
  console.log(window.n); // 5  n相当于window对象的属性
  console.log(window.a); // 1  a相当于window对象的属性
</script>
1
2
3
4
5
6
7
8
9

每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。

而在函数执行之后,栈将被环境弹出,把控制权返回给之前的执行环境。

# JS 中作用域的分类

JS 中有 3 种类型的作用域:

  • 全局作用域
    • 编写在 script 标签中的 js 代码(或单独 js 文件),都是在全局作用域中。
  • 局部作用域(函数作用域)
    • 每调用一次函数就会创建一个新的私有函数作用域,形参和当前私有函数作用域中声明的变量都是私有变量,保存在内部的一个变量对象中。
    • 函数被调用时创建函数作用域,函数执行完毕后,函数作用域被销毁,保存在其中的变量和函数定义了随之被销毁(闭包除外,只有当闭包函数的引用次数为 0 时,闭包函数和闭包中的变量被销毁)
    • 函数里能访问函数外变量,但函数外部是不能访问函数里面的变量,闭包除外,闭包函数会记住它在定义时所处的环境
    <script>
      function fn(a, b) {
        var c = 10;
        console.log((a + b) * c);
      }
      fn(1, 2); //函数调用创建函数作用域,代码执行用,作用域和变量a,b,c销毁
      fn(2, 3); //函数调用创建函数作用域,代码执行用,作用域和变量a,b,c销毁
    </script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    <script>
      function checkWeight(weight) {
        return function (_weight) {
      	  weight > _weight ? alert("过胖") : alert("ok达标");
        };
      }
      var P1 = checkWeight(100); // 调用完毕,作用域和变量weight不会被销毁
      P1(110); // 调用完毕,作用域和变量_weight会被销毁
    
      // 如果我们在最后加上P1 = null,则垃圾回收器回在下一次清理内存时
      //销毁掉 checkWeight 调用形成的作用域和作用域中的变量 weight。
    </script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
  • 块级作用域
    • 使用 let 或 const 关键字声明的变量,会形成块级作用域。
    {
      let a = 1;
    }
    console.log(a); // 会报错,{}里是块级作用域,外面是访问不到里面的变量的
    
    for (let i = 0; i < 3; i++) {
      console.log(i); // 0 1 2
    }
    console.log(i); // i is not defined
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

# 作用域链

当代码在一个环境中执行时,会创建变量对象的一个作用域链(作用域形成的链条)

  • 作用域链的前端,始终都是当前执行的代码所在环境的变量对象
  • 作用域链中的下一个对象来自于外部环境,再下一个变量对象则来自下下一个外部环境,一直到全局执行环境
  • 全局执行环境的变量对象始终都是作用域链上的最后一个对象

作用域链查找:

内部环境可以通过作用域链访问所有外部环境,但外部环境不能访问内部环境的任何变量和函数。

  • 在内部函数中,需要访问一个变量的时候,首先会访问函数本身的变量对象,是否有这个变量,如果没有,那么会继续沿作用域链往上查找
  • 如果在某个变量对象中找到则使用该变量对象中的变量值,如果没有找到,则会一直找到全局作用域。如果最后还找不到,就会报错。
<script>
  var a = 1;
  var c = 4;
  function fn1() {
    var a = 2;
    var b = 3;
    function fn2() {
      var b = 2;
      console.log(a); // 2  自身没有,沿着作用域链向上找
      console.log(b); //2  自身有,就用自身的
      console.log(c); //4  自身没有,沿着作用域链向上找,直到全局作用域中找到c=4
    }
    fn2();
  }
  fn1();
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 怎么理解 JS 静态作用域和动态作用域

  • 静态作用域:又称词法作用域,是指作用域在词法阶段就被确定了(函数定义的位置就决定了函数的作用域)不会改变,javascript 采用的是词法作用域。
  • 动态作用域:函数的作用域在函数调用时才决定的。
<script>
  var a = 1;
  function fn() {
    console.log(a);
  }
  function test() {
    var a = 2;
    fn();
  }
  test(); // 1
</script>
1
2
3
4
5
6
7
8
9
10
11

说明 fn 中打印的是全局下的 a ,这也印证了 JavaScript 使用了静态作用域。

静态作用域执行过程

当执行 fn 函数时,先从内部的AO对象查找是否有a变量,如果没有,沿着作用域链往上查找(由于JavaScript是词法作用域),上层为全局GO,所以结果打印1

# 7、浅拷贝

浅拷贝对象类型在赋值的过程中其实是复制了地址,从而会导致改变了一方其他也都被改变的情况;首先可以通过 Object.assign 来解决这个问题,很多人认为这个函数是用来深拷贝的。其实并不是,Object.assign 只会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址,所以并不是深拷贝。

# 8、深拷贝

简单的深拷贝可以通过: JSON.parse(JSON.stringify(object)) 来解决,该方法的局限性在:

  1. 会忽略 undefined
  2. 会忽略 symbol
  3. 不能序列化函数
  4. 不能解决循环引用的对象

如果你所需拷贝的对象含有内置类型并且不包含函数,可以使用 MessageChannel,完全的深拷贝只能是采用递归遍历了,以下是一个简单实现:

function deepClone(obj) {
  // 检查是否为基本数据类型或 null
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // 创建一个新的对象或数组
  const clone = Array.isArray(obj) ? [] : {};

  // 遍历对象的属性
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 递归深拷贝子属性
      clone[key] = deepClone(obj[key]);
    }
  }

  return clone;
}

const obj = {
  name: 'John',
  age: 30,
  address: {
    city: 'New York',
    country: 'USA'
  }
};

const cloneObj = deepClone(obj);
console.log(cloneObj);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

上述实现是一个简化版的深拷贝函数,适用于大多数情况。然而,对于一些特殊情况,例如包含函数、循环引用或原型链属性的对象,可能需要额外的处理来确保正确的深拷贝。在实际应用中,可以根据具体需求对深拷贝函数进行进一步的扩展和优化。

# 9、instanceof

关于 instanceof 其实表示的就是一种继承关系,或者原型链的结构。Instanceof 运算符的第一个变量是一个对象,暂时称为A;第二个变量一般是一个函数,暂时称为 B。Instanceof的判断规则是:沿着 A 的 proto 这条线来找,同时沿着 B 的 prototype 这条线来找,如果两条线能找到同一个引用,即同一个对象,那么就返回 true。如果找到终点还未重合,则返回 false。

# 10、关于原型,需要记住这张图

JavaScript 原型链是一种对象之间的关联机制,它是基于原型(prototype)的继承模型。每个 JavaScript 对象都有一个原型(prototype),并且可以通过原型链来访问和继承其他对象的属性和方法。

原型

JavaScript 对象通过__proto__ 指向父类对象,直到指向Object对象为止,这样就形成了一个原型指向的链条, 即原型链。

原型链可以用于以下几个方面:

  1. 继承:通过原型链,可以实现对象之间的继承关系。一个对象可以作为另一个对象的原型,从而继承原型对象的属性和方法。
  2. 属性和方法的共享:原型链使得多个对象可以共享同一个原型对象的属性和方法,避免了重复定义和占用额外的内存空间。
  3. 对象的扩展:通过修改原型对象,可以动态地为对象添加新的属性和方法,从而实现对象的扩展。
  4. 原型链的访问和修改:可以通过 Object.getPrototypeOf() 和 Object.setPrototypeOf() 方法来访问和修改对象的原型链。

总之,原型链是 JavaScript 中实现继承和属性共享的重要机制,它使得对象可以通过原型链向上查找属性和方法,实现代码的重用和扩展。

# 11、 var、let、const的区别

  1. 函数提升优先于变量提升,函数提升会把整个函数挪到作用域顶部,变量提升只会把声明挪到作用域顶部
  2. var 存在提升,我们能在声明之前使用。let、const 因为暂时性死区的原因,不能在声明前使用
  3. var 在全局作用域下声明变量会导致变量挂载在 window 上,其他两者不会
  4. let 和 const 作用基本一致,但是后者声明的变量不能再次赋值

const 定义的值一定是不能改变的吗?

  • const 实际上保证的,并不是变量的值不得改动,而是变量指向的那个栈内存地址所保存的数据不得改动。
  • 对于简单类型的数据(数值、字符串、布尔值)值就保存在变量指向的那个栈内存地址,因此等同于常量。
  • 引用类型的数据(主要是对象和数组)变量指向的栈内存地址,保存的只是一个指向实际数据的指针
  • const 只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。

所以如果是 const 声明的是一个引用类型的变量,其引用类型的结构是可以发生改变的。

例如:用const 声明一个数组,还能继续 push 元素到数组中;

  • 因为 const 声明的变量保存的只是栈内存中的地址,只是一个指向实际数据的指针。指针指向堆内存中保存的数据。
  • const 只能保证栈内存中的地址不变,但是堆内存中的数据如何改变是没有办法控制的。
  • push 方法相当于是改变了堆内存中的数据结构。

# 12、组合继承

核心是在子类的构造函数中通过 Parent.call(this) 继承父类的属性,然后改变子类的原型为 new Parent() 来继承父类的函数。

function Parent(value) {
  this.val = value
}
Parent.prototype.getValue = function() {
  console.log(this.val)
}
function Child(value) {
  Parent.call(this, value)
}
Child.prototype = new Parent()

const child = new Child(1)

child.getValue() // 1
child instanceof Parent // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这种继承方式优点在于构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数,但是也存在一个缺点就是在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费。

# 13、寄生组合继承

这种继承方式对组合继承进行了优化,组合继承缺点在于继承父类函数时调用了构造函数,我们只需要优化掉这点就行了。

function Parent(value) {
  this.val = value
}
Parent.prototype.getValue = function() {
  console.log(this.val)
}

function Child(value) {
  Parent.call(this, value)
}
Child.prototype = Object.create(Parent.prototype, {
  constructor: {
    value: Child,
    enumerable: false,
    writable: true,
    configurable: true
  }
})

// 或者
// Child.prototype = Object.create(Parent.prototype);
// Child.prototype.constructor = Child;

const child = new Child(1)

child.getValue() // 1
child instanceof Parent // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

以上继承实现的核心就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。

# 14、Class 继承

以上两种继承方式都是通过原型去解决的,在 ES6 中,我们可以使用 class 去实现继承:

class Parent {
  constructor(value) {
    this.val = value
  }
  getValue() {
    console.log(this.val)
  }
}
class Child extends Parent {
  constructor(value) {
    super(value)
    this.val = value
  }
}
let child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

class 实现继承的核心在于使用 extends 表明继承自哪个父类,并且在子类构造函数中必须调用 super,因为这段代码可以看成 Parent.call(this, value)。不过,之前也说了在 JS 中并不存在类,所以 class 的本质就是函数。

# 15、模块化

  1. 模块化带来的好处:解决命名冲突;提供复用性;提高代码可维护性
  2. 在早期,使用立即执行函数实现模块化是常见的手段,通过函数作用域解决了命名冲突、污染全局作用域的问题
  3. AMD 和 CMD
  4. CommonJS:CommonJS 最早是 Node 在使用,目前也仍然广泛使用,比如在 Webpack 中你就能见到它,当然目前在 Node 中的模块管理已经和 CommonJS 有一些区别了
  5. ES Module

# 16、Proxy

在 Vue3.0 中将会通过 Proxy 来替换原本的 Object.defineProperty 来实现数据响应式。 Proxy 是 ES6 中新增的功能,它可以用来自定义对象中的操作。如果需要实现一个 Vue 中的响应式,需要我们在 get 中收集依赖,在 set 派发更新,之所以 Vue3.0 要使用 Proxy 替换原本的 API 原因在于 Proxy 无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式的数据改变,唯一缺陷可能就是浏览器的兼容性不好了。

# 17、事件冒泡与捕获

浏览器里面的事件都会按照一定的规则去传递,不管 body 上绑定事件、或者 div 甚至 div 的 text 节点上绑定事件,这个事件必须先从根节点开始遍历(即Window对象开始),从上往下,传递的过程中,发现有的元素绑定了事件,也先放着,等全部事件捕获完毕(遍历完毕), 开始处理事件,处理的顺序为,从最小的根节点上的事件开始,依次向上冒泡,如图:

浏览器事件传递规则

一句话概括这种机制:

  • 捕获:自外而内,从根到叶,从大到小
  • 冒泡:自内而外,从叶到根,从小到大

还需要注意的是,并不是所有的事件都会冒泡,以下事件就没有:

  • onblur
  • onfocus
  • onmouseenter
  • onmouseleave

# 18、关于事件代理(事件委托)

事件代理是通过监听一个父元素,来给不同的子元素绑定事件,减少监听次数,从而提升浏览器速度。需要注意的是:如果元素被阻止冒泡了,千万别去用事件委托的方式监听事件,因为事件委托的原理是利用事件冒泡,当冒泡被阻止,就无法监听了。

# 回调和promise的区别

Promise和Callback都是用于处理异步操作的机制,但它们有一些区别。

回调(Callback): 回调是一种传递函数作为参数的方式,用于在异步操作完成后执行特定的逻辑。 回调函数通常在异步操作完成时被调用,接收结果或错误作为参数。 回调函数的执行顺序和上下文可能难以控制,尤其在处理多个嵌套的回调时,可能导致回调地狱(Callback Hell)的问题。 回调函数没有内置的错误处理机制,需要手动处理错误。 示例使用回调的代码:

function fetchData(callback) {
  // 模拟异步操作
  setTimeout(() => {
    const data = 'Hello, world!';
    callback(null, data); // 传递结果给回调函数
  }, 1000);
}

fetchData((error, result) => {
  if (error) {
    console.error('Error:', error);
  } else {
    console.log('Result:', result);
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Promise: Promise 是一种更为现代化的异步编程模式,它提供了一种更结构化和可读性更高的方式来处理异步操作。 Promise 是一个对象,代表一个异步操作的最终完成或失败的状态,并返回相应的结果或错误。 Promise 可以链式调用,通过 then() 方法处理操作成功的情况,通过 catch() 方法处理操作失败的情况。 Promise 提供了内置的错误处理机制,可以通过 catch() 方法捕获和处理错误,或者通过 finally() 方法执行清理操作。 示例使用 Promise 的代码:

function fetchData() {
  return new Promise((resolve, reject) => {
    // 模拟异步操作
    setTimeout(() => {
      const data = 'Hello, world!';
      resolve(data); // 完成操作并传递结果
      // 或者在出现错误时使用 reject(error) 来拒绝 Promise
    }, 1000);
  });
}

fetchData()
  .then(result => {
    console.log('Result:', result);
  })
  .catch(error => {
    console.error('Error:', error);
  });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

总结:

回调是传递函数作为参数的方式,用于处理异步操作,但容易导致代码难以维护和阅读。 Promise 是一种更为结构化和可读性更高的异步编程模式,提供了内置的错误处理和链式调用的能力,使代码更具表达力和可维护性。